package nl.Viewer.ViewManager.Objects;

import java.awt.Color;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.PixelGrabber;
import java.io.File;
import java.util.ArrayList;

import javax.media.opengl.GL;

/**
 * A collection of helper routines used in rendering several kinds of views. Large part of this class is used for
 * calculating the intersection coordinates of a plane with a bounding box to draw the plane in the box. (accessible
 * through planeToBoundingBoxIntersection)
 * 
 * @author Jeroen Verschuur
 * 
 */

public class RendererHelperFunctions {
   public static final double RAD2DEG = 57.295779513082320876798154814105;

   public static double[] normalize( double[] v1 ) {
      double length = (double)Math.sqrt( ( v1[0] * v1[0] ) + ( v1[1] * v1[1] ) + ( v1[2] * v1[2] ) );
      return new double[] { ( v1[0] / length ), ( v1[1] / length ), ( v1[2] / length ) };
   }

   public static float twist( double[] viewUp ) {
      return vectorAngle( new double[] { 0, 1, 0 }, viewUp );
   }

   public static float vectorAngle( double v1[], double v2[] ) {
      v1 = normalize( v1 );
      v2 = normalize( v2 );

      // dot product
      double dot = ( v1[0] * v2[0] ) + ( v1[1] * v2[1] ) + ( v1[2] * v2[2] );

      double angle = Math.acos( dot ) * RAD2DEG;
      return (float)angle;
   }

   /**
    * This method will return the elevation of a vector between two specified points (in degrees)
    * 
    * @param center
    * @param eye
    * @return
    */
   public static float elevation( double center[], double[] eye ) {
      double vec[] = new double[] { eye[0] - center[0], eye[1] - center[1], eye[2] - center[2] };
      if( vec[1] == 0 )
         return 0;

      double a = (double)Math.sqrt( ( vec[0] * vec[0] ) + ( vec[2] * vec[2] ) );
      a /= vec[1];

      return (float)( 90 - ( (double)Math.atan( a ) * RAD2DEG ) );
   }

   /**
    * This method will return the azimuth of the vector between two specified points (in degrees).
    * 
    * @param center
    * @param eye
    * @return
    */
   public static float azimuth( double center[], double[] eye ) {
      double vec[] = new double[] { eye[0] - center[0], eye[1] - center[1], eye[2] - center[2] };

      if( vec[2] == 0 )
         return 0;
      return (float)( Math.atan( vec[0] / vec[2] ) * RAD2DEG );
   }

   public static double normalElevation( double[] normal ) {
      // if( normal[1] == 0 )
      // return 0;
      //
      // double a = Math.sqrt( ( normal[0] * normal[0] ) + ( normal[2] * normal[2] ) );
      // a /= normal[1];
      //
      // return Math.atan( a );

      double div = Math.sqrt( normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2] );
      double acos = Math.acos( normal[1] / div );

      return Math.PI / 2 - acos;
   }

   public static double normalAzimuth( double[] normal ) {
      return Math.atan2( normal[0], normal[2] );
   }

   public static void polarToCartesian( double elevation, double azimuth, double[]vector ) {
      vector[0] = Math.cos( elevation ) * Math.sin( azimuth );
      vector[1] = Math.sin( elevation );
      vector[2] = Math.cos( elevation ) * Math.cos( azimuth );
   }

   public static void drawBox( GL gl, float xsize, float ysize, float zsize ) {
      gl.glColor4f( 1, 1, 1, 1 );
      gl.glLineWidth( 2 );
      gl.glBegin( GL.GL_LINE_LOOP );
      // gl.glTexCoord2f(0.0f, 0.0f);
      gl.glVertex3f( -( xsize / 2 ), +( ysize / 2 ), 0 );
      // gl.glTexCoord2f(1.0f, 0.0f);
      gl.glVertex3f( ( xsize / 2 ), +( ysize / 2 ), 0 );
      // gl.glTexCoord2f(1.0f, 1.0f);
      gl.glVertex3f( ( xsize / 2 ), -( ysize / 2 ), 0 );
      // gl.glTexCoord2f(0.0f, 1.0f);
      gl.glVertex3f( -( xsize / 2 ), -( ysize / 2 ), 0 );
      gl.glEnd();

      gl.glBegin( GL.GL_LINE_LOOP );
      // gl.glTexCoord2f(0.0f, 0.0f);
      gl.glVertex3f( -( xsize / 2 ), +( ysize / 2 ), -( zsize / 2 ) );
      // gl.glTexCoord2f(1.0f, 0.0f);
      gl.glVertex3f( ( xsize / 2 ), +( ysize / 2 ), -( zsize / 2 ) );
      // gl.glTexCoord2f(1.0f, 1.0f);
      gl.glVertex3f( ( xsize / 2 ), -( ysize / 2 ), -( zsize / 2 ) );
      // gl.glTexCoord2f(0.0f, 1.0f);
      gl.glVertex3f( -( xsize / 2 ), -( ysize / 2 ), -( zsize / 2 ) );
      gl.glEnd();

      gl.glBegin( GL.GL_LINE_LOOP );
      // gl.glTexCoord2f(0.0f, 0.0f);
      gl.glVertex3f( -( xsize / 2 ), +( ysize / 2 ), ( zsize / 2 ) );
      // gl.glTexCoord2f(1.0f, 0.0f);
      gl.glVertex3f( ( xsize / 2 ), +( ysize / 2 ), ( zsize / 2 ) );
      // gl.glTexCoord2f(1.0f, 1.0f);
      gl.glVertex3f( ( xsize / 2 ), -( ysize / 2 ), ( zsize / 2 ) );
      // gl.glTexCoord2f(0.0f, 1.0f);
      gl.glVertex3f( -( xsize / 2 ), -( ysize / 2 ), ( zsize / 2 ) );
      gl.glEnd();

      gl.glBegin( GL.GL_LINES );
      gl.glVertex3f( -( xsize / 2 ), -( ysize / 2 ), -( zsize / 2 ) );
      gl.glVertex3f( -( xsize / 2 ), -( ysize / 2 ), ( zsize / 2 ) );

      gl.glVertex3f( -( xsize / 2 ), ( ysize / 2 ), -( zsize / 2 ) );
      gl.glVertex3f( -( xsize / 2 ), ( ysize / 2 ), ( zsize / 2 ) );

      gl.glVertex3f( ( xsize / 2 ), -( ysize / 2 ), -( zsize / 2 ) );
      gl.glVertex3f( ( xsize / 2 ), -( ysize / 2 ), ( zsize / 2 ) );

      gl.glVertex3f( ( xsize / 2 ), ( ysize / 2 ), -( zsize / 2 ) );
      gl.glVertex3f( ( xsize / 2 ), ( ysize / 2 ), ( zsize / 2 ) );

      gl.glEnd();

   }

   public static void drawBox( GL gl, double xstart, double xend, double ystart, double yend, double zstart, double zend ) {
      gl.glColor4f( 1, 1, 1, 1 );
      gl.glLineWidth( 1 );
      gl.glBegin( GL.GL_LINE_LOOP );
      // gl.glTexCoord2f(0.0f, 0.0f);
      gl.glVertex3d( xstart, ystart, zstart );
      // gl.glTexCoord2f(1.0f, 0.0f);
      gl.glVertex3d( xend, ystart, zstart );
      // gl.glTexCoord2f(1.0f, 1.0f);
      gl.glVertex3d( xend, ystart, zend );
      // gl.glTexCoord2f(0.0f, 1.0f);
      gl.glVertex3d( xstart, ystart, zend );
      gl.glEnd();

      gl.glBegin( GL.GL_LINE_LOOP );
      // gl.glTexCoord2f(0.0f, 0.0f);
      gl.glVertex3d( xstart, yend, zstart );
      // gl.glTexCoord2f(1.0f, 0.0f);
      gl.glVertex3d( xend, yend, zstart );
      // gl.glTexCoord2f(1.0f, 1.0f);
      gl.glVertex3d( xend, yend, zend );
      // gl.glTexCoord2f(0.0f, 1.0f);
      gl.glVertex3d( xstart, yend, zend );
      gl.glEnd();

      gl.glBegin( GL.GL_LINES );
      gl.glVertex3d( xstart, ystart, zstart );
      gl.glVertex3d( xstart, yend, zstart );

      gl.glVertex3d( xend, ystart, zstart );
      gl.glVertex3d( xend, yend, zstart );

      gl.glVertex3d( xend, ystart, zend );
      gl.glVertex3d( xend, yend, zend );

      gl.glVertex3d( xstart, ystart, zend );
      gl.glVertex3d( xstart, yend, zend );

      gl.glEnd();

   }

   public static double[] crossProduct( double[] vec1, double[] vec2 ) {
      return new double[] { ( vec1[1] * vec2[2] ) - ( vec1[2] * vec2[1] ), ( vec1[2] * vec2[0] ) - ( vec1[0] * vec2[2] ), ( vec1[0] * vec2[1] ) - ( vec1[1] * vec2[0] ) };

   }

   public static double dot( double[] u, double[] v ) {
      return( u[0] * v[0] + u[1] * v[1] + u[2] * v[2] );
   }

   public static double[] linePlaneIntersectionTest( double[] p0, double[] p1, double[] V0, double[] Pnormal ) {
      double[] u = new double[] { p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2] };
      double[] w = new double[] { p0[0] - V0[0], p0[1] - V0[1], p0[2] - V0[2] };

      double D = dot( Pnormal, u );
      double N = -dot( Pnormal, w );

      if( Math.abs( D ) < 0.000001 ) { // segment is parallel to plane
         if( N == 0 ) // segment lies in plane
            return p0; // p0 AND p1 in plane, return both?
         else
            return null; // no intersection
      }
      // they are not parallel
      // compute intersection param
      double sI = N / D;
      if( sI < 0 || sI > 1 )
         return null; // no intersection

      // compute segment intersection point
      double[] I = new double[ 3 ];
      I[0] = p0[0] + ( sI * u[0] );
      I[1] = p0[1] + ( sI * u[1] );
      I[2] = p0[2] + ( sI * u[2] );
      return I;
   }

   public static void addVertex( ArrayList<double[]> vertices, double[] newVertex ) {
      if( newVertex == null )
         return;
      for( int i = 0; i < vertices.size(); i++ ) {
         double[] v = vertices.get( i );
         if( v[0] == newVertex[0] && v[1] == newVertex[1] && v[2] == newVertex[2] ) {
            return;
         }
      }
      vertices.add( newVertex );
   }

   public static double[] createVector( double[] p0, double[] p1 ) {
      return new double[] { p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2] };
   }

   public static double[] findLeastParallelAxis( double[] normal ) {
      double[] xVec = new double[] { 1, 0, 0 };
      double[] yVec = new double[] { 0, 1, 0 };
      double[] zVec = new double[] { 0, 0, 1 };

      double dotX, dotY, dotZ;
      dotX = Math.abs( dot( normal, xVec ) );
      dotY = Math.abs( dot( normal, yVec ) );
      dotZ = Math.abs( dot( normal, zVec ) );

      if( dotX <= dotY )
         if( dotX <= dotZ )
            return xVec;
         else if( dotY <= dotZ )
            return yVec;

      return zVec;

   }

   public static double[] projectOntoPlane( double[] vec, double[] normal ) {
      // the normal should already be normalized!
      double dotVecNormal = dot( vec, normal );
      return new double[] { vec[0] - normal[0] * dotVecNormal, vec[1] - normal[1] * dotVecNormal, vec[2] - normal[2] * dotVecNormal };
   }

   public static double[] getDirectionCosines( double[] normal ) {
      double[] center = new double[] { 0, 0, 0 };
      System.out.println( "elevation: " + elevation( center, normal ) );
      System.out.println( "Azimuth: " + azimuth( center, normal ) );

      double[] n = normal.clone();
      normalize( n );
      double[] leastParallelAxis = findLeastParallelAxis( n );
      double[] projectedXAxis = projectOntoPlane( leastParallelAxis, n );
      normalize( projectedXAxis );
      double[] projectedYAxis = crossProduct( projectedXAxis, n );
      normalize( projectedYAxis );

      System.out.println( "normal: " + n[0] + "/" + n[1] + "/" + n[2] + ", " + projectedXAxis[0] + "/" + projectedXAxis[1] + "/" + projectedXAxis[2] + ", " + projectedYAxis[0] + "/" + projectedYAxis[1] + "/" + projectedYAxis[2] );

      return new double[] { projectedXAxis[0], projectedXAxis[1], projectedXAxis[2], projectedYAxis[0], projectedYAxis[1], projectedYAxis[2], n[0], n[1], n[2] };
   }

   private static void sortVertices( ArrayList<double[]> leftVertices, ArrayList<double[]> sortedVertices,
         double[] origin ) {
      if( leftVertices.size() == 0 )
         return;
      if( leftVertices.size() == 1 ) {
         sortedVertices.add( leftVertices.remove( 0 ) );
         return;
      }

      double[] chosenVertex = sortedVertices.get( sortedVertices.size() - 1 );
      double smallestAngle = 2 * Math.PI;

      double[] chosenVertexVector = normalize( createVector( chosenVertex, origin ) );
      int nextChosen = 0;
      for( int i = 0; i < leftVertices.size(); i++ ) {
         double dp = Math.acos( dot( chosenVertexVector, normalize( createVector( leftVertices.get( i ), origin ) ) ) );
         if( dp < smallestAngle ) {
            smallestAngle = dp;
            nextChosen = i;
         }
      }

      // select new vertex
      sortedVertices.add( leftVertices.remove( nextChosen ) );

      sortVertices( leftVertices, sortedVertices, origin );
   }

   public static ArrayList<double[]> planeToBoundingBoxIntersection( double[] normal, double[] origin, double[] bounds ) {
      ArrayList<double[]> vertices = new ArrayList<double[]>();
      normal = normalize( normal );

      // set-up the corners of the bounding box
      double[] A = new double[] { bounds[0], bounds[2], bounds[4] };
      double[] B = new double[] { bounds[0], bounds[2], bounds[5] };
      double[] C = new double[] { bounds[1], bounds[2], bounds[5] };
      double[] D = new double[] { bounds[1], bounds[2], bounds[4] };
      double[] E = new double[] { bounds[0], bounds[3], bounds[4] };
      double[] F = new double[] { bounds[0], bounds[3], bounds[5] };
      double[] G = new double[] { bounds[1], bounds[3], bounds[5] };
      double[] H = new double[] { bounds[1], bounds[3], bounds[4] };

      // check if a line has intersection with plane
      addVertex( vertices, linePlaneIntersectionTest( A, B, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( B, C, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( C, D, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( D, A, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( E, F, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( F, G, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( G, H, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( H, E, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( A, E, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( B, F, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( C, G, origin, normal ) );
      addVertex( vertices, linePlaneIntersectionTest( D, H, origin, normal ) );

      if( vertices.size() == 0 )
         return null;

      ArrayList<double[]> sortedVertices = new ArrayList<double[]>();
      sortedVertices.add( vertices.remove( 0 ) );
      sortVertices( vertices, sortedVertices, origin );

      return sortedVertices;
   }

   /**
    * This method will calculate the correct camera position to get a complete view of a given bounding box. Most of the
    * code has been copied from the vtkRenderer source of the Visualization Toolkit (VTK).
    * 
    */
   public static void resetCamera( double viewAngle, double[] bounds, Double[] eye, Double[] center ) {
      double w1 = bounds[1] - bounds[0];
      double w2 = bounds[3] - bounds[2];
      double w3 = bounds[5] - bounds[4];

      center[0] = w1 / 2;
      center[1] = w2 / 2;
      center[2] = w3 / 2;

      w1 *= w1;
      w2 *= w2;
      w3 *= w3;
      double radius = w1 + w2 + w3;

      // If we have just a single point, pick a radius of 1.0
      radius = ( radius == 0 ) ? ( 1.0 ) : ( radius );

      // compute the radius of the enclosing sphere
      radius = Math.sqrt( radius ) * 0.5;

      // default so that the bounding sphere fits within the view fustrum

      // compute the distance from the intersection of the view frustum with the
      // bounding sphere. Basically in 2D draw a circle representing the bounding
      // sphere in 2D then draw a horizontal line going out from the center of
      // the circle. That is the camera view. Then draw a line from the camera
      // position to the point where it intersects the circle. (it will be tangent
      // to the circle at this point, this is important, only go to the tangent
      // point, do not draw all the way to the view plane). Then draw the radius
      // from the tangent point to the center of the circle. You will note that
      // this forms a right triangle with one side being the radius, another being
      // the target distance for the camera, then just find the target dist using
      // a sin.
      double distance = radius / Math.sin( viewAngle * Math.PI / 360.0 );

      // update the camera

      eye[0] = center[0] + distance * 0;
      eye[1] = center[1] + distance * 0;
      eye[2] = center[2] + distance * 1;
   }

   /**
    * An example of drawing dots.
    */
   protected void drawSomeDots( GL gl ) {
      gl.glBegin( GL.GL_POINTS );
      gl.glVertex2i( 100, 50 );
      gl.glVertex2i( 100, 130 );
      gl.glVertex2i( 150, 130 );
      gl.glEnd();
   }

   /**
    * An example of drawing lines
    */
   protected void drawSomeLines( GL gl ) {
      gl.glBegin( GL.GL_LINES );
      gl.glVertex2i( 50, 200 );
      gl.glVertex2i( 75, 250 );
      // same thing, but over ten pixels to the right
      gl.glVertex2i( 60, 200 );
      gl.glVertex2i( 85, 250 );
      gl.glEnd();
   }

   /**
    * An example of drawing an open polyline. The last point does NOT connect back to the first
    */
   protected void drawOpenPoly( GL gl ) {
      gl.glBegin( GL.GL_LINE_STRIP );
      gl.glVertex2i( 100, 200 );
      gl.glVertex2i( 150, 250 );
      gl.glVertex2i( 100, 250 );
      gl.glVertex2i( 150, 200 );
      gl.glEnd();
   }

   /**
    * An example of drawing an open polyline. The last point connects back to the first
    */
   protected void drawClosedPoly( GL gl ) {
      gl.glBegin( GL.GL_LINE_LOOP );
      gl.glVertex2i( 200, 200 );
      gl.glVertex2i( 250, 250 );
      gl.glVertex2i( 200, 250 );
      gl.glVertex2i( 250, 200 );
      gl.glEnd();
   }

   /**
    * An example of drawing triangles with GL_TRIANGLES. Each three points is a new triangle.
    */
   protected void drawTriangles( GL gl ) {
      gl.glBegin( GL.GL_TRIANGLES );

      gl.glVertex2i( 400, 50 );
      gl.glVertex2i( 400, 100 );
      gl.glVertex2i( 420, 75 );

      gl.glVertex2i( 425, 50 );
      gl.glVertex2i( 425, 100 );
      gl.glVertex2i( 445, 75 );

      gl.glEnd();
   }

   /**
    * Draws a rectangle with the given AWT color
    */
   protected void drawFilledRect( GL gl, Color c ) {
      float redF = c.getRed() / 255f;
      float greenF = c.getGreen() / 255f;
      float blueF = c.getBlue() / 255f;
      gl.glColor3f( redF, greenF, blueF );
      gl.glRecti( 200, 50, 250, 150 );
      // now reset the color to black
      gl.glColor3f( 0.0f, 0.0f, 0.0f );
   }

   /**
    * Draws a polygon with the given AWT color
    */
   protected void drawFilledPoly( GL gl, Color c ) {
      float redF = c.getRed() / 255f;
      float greenF = c.getGreen() / 255f;
      float blueF = c.getBlue() / 255f;
      gl.glColor3f( redF, greenF, blueF );
      gl.glBegin( GL.GL_POLYGON );
      gl.glVertex2i( 300, 50 );
      gl.glVertex2i( 350, 60 );
      gl.glVertex2i( 375, 100 );
      gl.glVertex2i( 325, 115 );
      gl.glVertex2i( 300, 75 );
      gl.glEnd();
      // now reset the color to black
      gl.glColor3f( 0.0f, 0.0f, 0.0f );
   }

   /**
    * Method to convert from BGRA to RGBA.
    * 
    * @param pixel
    * @return
    */
   public static int convertPixel( int pixel ) {
      int r, g, b, a;
      a = ( ( pixel >> 24 ) & 0xff );
      r = ( ( pixel >> 16 ) & 0xff );
      g = ( ( pixel >> 8 ) & 0xff );
      b = ( ( pixel >> 0 ) & 0xff );

      return ( a << 24 ) + ( b << 16 ) + ( g << 8 ) + ( r << 0 );
   }

   public static int[][] loadImage( String image ) throws Exception {
      File f = new File( image );
      if( !f.exists() ) {
         throw new Exception( "File not found: " + image );
      }

      Image im = Toolkit.getDefaultToolkit().createImage( image );
      int wait = 200;
      while( im.getHeight( null ) < 0 && wait > 0 ) {
         try {
            Thread.sleep( 1 );
         }
         catch( Exception e ) {
         }
      }

      if( im.getHeight( null ) < 0 )
         throw new Exception( "Image could not be opened" );

      int w = im.getWidth( null );
      int h = im.getHeight( null );

      int[] pixels = new int[ w * h ];
      PixelGrabber pg = new PixelGrabber( im, 0, 0, w, h, pixels, 0, w );

      pg.grabPixels();

      int[][] result = new int[ w ][ h ];
      for( int i = 0; i < h; i++ ) {
         for( int j = 0; j < w; j++ ) {
            result[j][i] = convertPixel( pixels[( i * w ) + j] );
         }
      }
      return result;
   }

}
